🧠 Qwen3.8 MTP · 按源码拆解 先看权重组成,再跑主干、草稿、验证

按代码执行顺序讲: ① 权重加载 → ② 主干推理(1次)→ ③ 草稿循环(只跑1层)→ ④ 验证推理(再跑1次主干)→ ⑤ 接受/拒绝

📂 ① 权重组成 模型文件里就这 3 块

GGUF 里按层索引存储,加载后得到三个东西:主干(64层)、MTP Head(1层)、共享的 LM_Head。
graph TD W["权重文件 (qwen3.8.gguf)"] --> T["trunk = weights[0:64]
主干 Trunk (64层标准Decoder, 27B)"] W --> M["mtp = weights[64]
MTP Head (1层轻量Decoder, 0.45B)"] W --> L["lm = weights.lm_head
共享 LM Head (输出层)"] style T fill:#f0fdf4,stroke:#16a34a,stroke-width:2px style M fill:#fef9c3,stroke:#ca8a04,stroke-width:2px style L fill:#dbeafe,stroke:#3b82f6,stroke-width:2px
# 加载权重(这就是全部)
trunk = weights[0:64]   # 主干 64层(标准 Decoder Block)
mtp   = weights[64]     # MTP Head 1层(轻量 Decoder Block)
lm    = weights.lm_head # 共享 LM Head(词表映射)

# 后续所有流程都复用这三块,草稿和验证共用 trunk 和 lm。
关键: 所有推理流程都基于这 3 个组件 拼装。草稿和验证共用 trunk 和 lm,唯一多出来的是 mtp 这 1 层。
✅ 实测核对(官方 Qwen/Qwen3.8-27B config.json): num_hidden_layers=64 ✓、 mtp_num_hidden_layers=1 ✓(MTP 确为 1 层)、 mtp_use_dedicated_embeddings=false ✓(MTP 直接复用主模型共享 token 嵌入,与"取前一个 token 的 embedding 拼接"一致)、 tie_word_embeddings=false(LM Head 为独立矩阵,被草稿 / 验证两条路径共用)。 结论:本文"64 层主干 + 1 层 MTP Head + 共享 LM Head"的三组件拆解与真实模型权重结构一致。

⚙️ ② 主干推理(先跑 1 次) 产出隐藏状态 h,后续复用

整个推理过程中,只有这一步是“重”计算(过全部 64 层)。之后所有草稿循环都再也不碰主干。
graph TD Ctx["当前上下文 (已生成的 token)"] --> Trunk["trunk (64层标准Decoder)"] Trunk --> H["产出隐藏状态 h (缓存)"] H --> Next["后续草稿和验证都复用 h"] style Trunk fill:#f0fdf4,stroke:#16a34a,stroke-width:3px style H fill:#fef9c3,stroke:#ca8a04,stroke-width:2px
# 主干推理(仅此一次)
def trunk_forward(context):
    h = embed(context)          # token -> embedding
    for layer in trunk:         # 循环 64 层
        h = layer(h)
    return h                    # 隐藏状态 h (seq, 4096)

# 在一个生成步中,只调用一次
h = trunk_forward(context)      # ← 这就是唯一的“重”计算
cache_h = h                     # 缓存起来,后续草稿用
重点: h 被缓存后,草稿阶段完全不再调用 `trunk`,只复用这个 h。

⚡ ③ 草稿推理(循环 N 次) 只跑 MTP Head(1层)+ LM_Head,完全不碰主干

这是加速的核心:草稿循环里 没有 `trunk`,只有 mtp 和 lm,代价是 1 层 vs 64 层。
graph TD H["缓存的隐藏状态 h"] --> MTP1["mtp (1层) + 上一token的embedding"] MTP1 --> LM1["lm (共享 LM_Head)"] LM1 --> T1["草稿 token1"] T1 --> MTP2["mtp (1层) + token1的embedding"] MTP2 --> LM2["lm (共享 LM_Head)"] LM2 --> T2["草稿 token2"] style H fill:#fef9c3,stroke:#ca8a04 style MTP1 fill:#fef9c3,stroke:#ca8a04,stroke-width:2px style MTP2 fill:#fef9c3,stroke:#ca8a04,stroke-width:2px style LM1 fill:#dbeafe,stroke:#3b82f6 style LM2 fill:#dbeafe,stroke:#3b82f6
# 草稿推理(循环 N 次,默认 2~3)
def draft_loop(h, prev_token, num_drafts=2):
    drafts = []
    prev_emb = embed(prev_token)    # 上一 token 的 embedding
    for _ in range(num_drafts):
        # ★ 关键:这里只有 mtp(1层)和 lm,没有 trunk!
        h_mtp = forward(mtp, concat(h, prev_emb))  # 只过 1 层
        logits = lm(h_mtp)                         # 过共享 LM_Head
        token = sample(logits)                     # 采样
        drafts.append(token)
        prev_emb = embed(token)      # 下一轮用刚猜出的 token
        h = h_mtp                    # 下一轮用 MTP 的输出
    return drafts

# 使用缓存的 h 和最后一个真实 token
drafts = draft_loop(h, last_real_token, num_drafts=2)
# 得到 [草稿1, 草稿2]
看清楚: 循环体里 完全没有 `trunk`。每次迭代只过 mtp(1 层)和 lm。这是草稿极快的根本原因。

✅ ④ 验证推理(再跑 1 次主干) 把上下文+草稿拼一起,一次性算真实分布

验证时再跑一次主干,但因为 KV Cache 已缓存上下文部分,只需要算新拼接的草稿位置,浪费极小。
graph TD Full["上下文 + 草稿 (拼成一个序列)"] --> Trunk2["trunk (64层)
带 KV Cache 复用"] Trunk2 --> H2["各位置隐藏状态"] H2 --> LM2["lm (共享 LM_Head)"] LM2 --> Real["真实概率分布 (verifier)"] style Trunk2 fill:#f0fdf4,stroke:#16a34a,stroke-width:3px style LM2 fill:#dbeafe,stroke:#3b82f6
# 验证推理(再跑一次主干)
def verify(context_with_drafts):
    # 第二次调用 trunk(带 KV Cache,只算新位置)
    h_full = forward(trunk, context_with_drafts)  # 上下文+草稿一起过
    logits = lm(h_full)                           # 每个位置的真实分布
    return logits

# 拼装完整序列
full_context = context + drafts
verify_logits = verify(full_context)
# 得到 [位置1的真实分布, 位置2的真实分布, ...]
注意: 这一步虽然又跑了一次 trunk(64层),但 KV Cache 让“上下文”部分完全跳过,只计算新增的草稿 token 位置,所以实际新增计算量远小于 64 层。

🎯 ⑤ 接受 / 拒绝 用真实分布修正草稿

逐个比对草稿和真实分布,正确的保留,错误的丢弃并用真实值替换。
def accept_reject(drafts, verify_logits):
    accepted = []
    for i, draft_token in enumerate(drafts):
        real_token = argmax(verify_logits[i])      # 真实分布的最大概率 token
        if draft_token == real_token:
            accepted.append(draft_token)           # 猜对了,收下
        else:
            accepted.append(real_token)            # 猜错了,用真实的替换
            break                                  # 遇到第一个错误就停
    return accepted

# 最终输出
final_tokens = accept_reject(drafts, verify_logits)
保证质量: 因为验证分布是真实模型算出来的,所以 最终输出的 token 永远和原始模型完全一致,错的草稿会被丢弃。

🔁 完整生成步总览 按源码顺序串起来

graph TD A["开始生成步"] --> B["① trunk_forward(上下文)
(64层, 只1次)"] B --> C["② draft_loop(缓存h)
(循环2~3次, 每次只过mtp+lm)"] C --> D["③ verify(上下文+草稿)
(再次trunk, 带KV Cache)"] D --> E["④ accept_reject(草稿, 真实分布)"] E --> F["输出接受的token, 进入下一步"] style B fill:#f0fdf4,stroke:#16a34a,stroke-width:2px style C fill:#fef9c3,stroke:#ca8a04,stroke-width:2px style D fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style E fill:#f3e8ff,stroke:#8b5cf6,stroke-width:2px
计算量拆解:
• 步骤①:1 次 64 层(重)
• 步骤②:2~3 次 1 层(极轻)
• 步骤③:1 次 64 层(重,但 KV Cache 复用)
= 2×64层 + 3×1层,远小于传统自回归的 3×64层。

🧪 ⑦ 实测验证 下载真实 Qwen3.8-27B 权重核对

不靠脑补:从 ModelScope 拉真实模型(unsloth/Qwen3.8-27B-GGUF,Q4_K_M 量化,16.46GB)+ 官方 config.json,用 gguf 解析器逐张量核对本文机制。
验证目标: 本文「64 层主干 + 1 层 MTP Head + 共享 LM Head」的三组件拆解,以及「草稿 = 取 h 拼接前一个 token 的 embedding 再过一层 fc」的机制,必须能在真实权重里找到对应张量。
验证步骤:
① 下载真实 GGUF:unsloth/Qwen3.8-27B-GGUF / Qwen3.8-27B-UD-Q4_K_M.gguf(16.46GB);
② 下载官方 config.json(Qwen/Qwen3.8-27B)核对架构超参;
③ 用 gguf.GGUFReader 解析张量名 / 形状 / 元数据,逐项比对。

📊 关键物证(来自真实 GGUF)

真实权重事实 对应本文说法 结论
block_count = 65
blk.0~63 主干 + blk.64 为 MTP
64 层主干 + 1 层 MTP Head ✅
nextn_predict_layers = 1
full_attention_interval = 4
MTP 只 1 层;主干为混合注意力 ✅
顶层仅一个 output.weight [5120, 248320](共享 LM Head)
一个 token_embd.weight [5120, 248320]
验证 / 草稿两条路径共用同一 lm ✅
blk.64.nextn.eh_proj.weight
shape = [10240, 5120]
取 h(5120) 拼接前一个 token 的 embedding(5120),再过一层 fc → 5120 ✅
blk.64 含完整 attn/ffn/norm(15 张量)+ 4 个 nextn 专属张量,其中 shared_head_norm MTP 输出经 shared_head_norm 复用同一 LM Head ✅
✅ 核对结论: 本文对 MTP 机制的描述(同权重双路径、主干 + 1 层 MTP 头、共享 LM Head、草稿侧拼接 embedding 再过 fc)与真实模型结构完全一致。真实规格:hidden 5120 / vocab 248320 / 64+1 层 / 混合注意力(每 4 层 1 个全注意力)。文中 4096、128k 等为教学示意值,机制正确。
📌 备注: 模型内部架构标记为 qwen35(Qwen3.5 系),社区 / GGUF 仓名为 "Qwen3.8",属命名差异,不影响机制结论。校验脚本见 verify_qwen38_mtp.py,验证归档见 tests/result/。
华杉的技术博客 · 苏ICP备2026063680号-1
首页